package com.yth.utils;

import com.yth.utils.ognl.OgnlHelper;
import lombok.AllArgsConstructor;
import lombok.Getter;
import ognl.OgnlException;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.builder.StaticSqlSource;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.scripting.defaults.RawSqlSource;
import org.apache.ibatis.scripting.xmltags.*;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;

import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Mybatis工具类
 *
 * @author yutianhong
 * @version 1.0
 * @since 2024/11/23 10:41
 */
public class MyBatisUtils {
    private static final Configuration CONFIGURATION;
    private static final Object[] ZERO_LENGTH_ARGS = new Object[0];

    /**
     * Pattern for non-escaped #{} and ${}
     */
    private static final Pattern MYBATIS_VARIABLE_PATTERN = Pattern.compile("(?<!\\\\)[#]\\{(.*?)\\}|(?<!\\\\)[\\$]\\{(.*?)\\}");

    private static final Field[] EMPTY_FIELD_ARRAY = new Field[0];
    private static final Map<Class<?>, Field[]> DECLARED_FIELDS_CACHE = new ConcurrentHashMap<>();

    static {
        // 初始化MyBatis的环境配置
        Environment environment = new Environment("development",
                new JdbcTransactionFactory(),
                new UnpooledDataSource());
        CONFIGURATION = new Configuration(environment);
    }

    @Getter
    @AllArgsConstructor
    public static final class SqlStatement {
        /**
         * SQL语句
         */
        private final String sql;
        /**
         * 参数数组，用于填充可执行的SQL
         */
        private final Object[] args;
    }

    /**
     * 用于将XML格式的SQL语句和参数进行解析，并返回渲染后的SQL和参数列表。
     *
     * @param xmlString       XML格式的SQL语句
     * @param parameterObject 参数对象，可以是Map，在SQL中用作参数
     * @return SQL语句和参数
     * @throws RuntimeException XML解析或SQL渲染出错时抛出
     */
    public static SqlStatement renderSql(String xmlString, Object parameterObject) {
        if (StringUtils.isBlank(xmlString)) {
            throw new IllegalArgumentException("xmlString is null or empty");
        }
        SqlSource sqlSource = parseSqlScript(xmlString);

        BoundSql boundSql;
        try {
            boundSql = sqlSource.getBoundSql(parameterObject);
        } catch (Exception e) {
            throw new RuntimeException("SQL模板与参数不匹配", e);
        }
        String sql = boundSql.getSql();
        Object[] args = parseArgs(boundSql);
        return new SqlStatement(sql, args);
    }

    /**
     * 从遵循mybatis xml规范的sql字符串中提取外部变量。<p>
     * 不保证能够全部提取出来，不保证全部提取出来的都是对的。
     *
     * @param xmlString xml字符串
     * @return 变量集合
     */
    public static Set<String> extractVariables(String xmlString) {
        if (StringUtils.isBlank(xmlString)) {
            return Collections.emptySet();
        }
        SqlSource sqlSource = parseSqlScript(xmlString);
        try {
            return extractVariables(sqlSource);
        } catch (Exception e) {
            throw new RuntimeException("提取参数错误, xmlString: " + xmlString, e);
        }
    }

    private static Set<String> extractVariables(SqlSource sqlSource) throws Exception {
        if (sqlSource instanceof RawSqlSource) {
            RawSqlSource rawSqlSource = (RawSqlSource) sqlSource;
            SqlSource innerSqlSource = ReflectUtils.getDeclaredFieldValue(RawSqlSource.class, "sqlSource", rawSqlSource);
            return extractVariables(innerSqlSource);
        } else if (sqlSource instanceof StaticSqlSource) {
            StaticSqlSource staticSqlSource = (StaticSqlSource) sqlSource;
            List<ParameterMapping> parameterMappings = ReflectUtils.getDeclaredFieldValue(StaticSqlSource.class, "parameterMappings", staticSqlSource);
            if (CollectionUtils.isEmpty(parameterMappings)) {
                return Collections.emptySet();
            }
            Set<String> set = new HashSet<>(parameterMappings.size());
            for (ParameterMapping mapping : parameterMappings) {
                set.add(mapping.getProperty());
            }
            return set;
        } else if (sqlSource instanceof DynamicSqlSource) {
            DynamicSqlSource dynamicSqlSource = (DynamicSqlSource) sqlSource;
            SqlNode rootSqlNode = ReflectUtils.getDeclaredFieldValue(DynamicSqlSource.class, "rootSqlNode", dynamicSqlSource);
            return extractVariables(rootSqlNode, new HashSet<>());
        } else {
            // 不处理其他SqlSource
            return Collections.emptySet();
        }
    }

    private static Set<String> extractVariables(SqlNode sqlNode, Set<String> set) throws Exception {
        if (sqlNode instanceof MixedSqlNode) {
            MixedSqlNode mixedSqlNode = (MixedSqlNode) sqlNode;
            List<SqlNode> contents = ReflectUtils.getDeclaredFieldValue(MixedSqlNode.class, "contents", mixedSqlNode);
            if (CollectionUtils.isNotEmpty(contents)) {
                for (SqlNode obj : contents) {
                    extractVariables(obj, set);
                }
            }
        } else if (sqlNode instanceof ForEachSqlNode) {
            ForEachSqlNode forEachSqlNode = (ForEachSqlNode) sqlNode;
            String collectionExpression = ReflectUtils.getDeclaredFieldValue(ForEachSqlNode.class, "collectionExpression", forEachSqlNode);
            Set<String> collectionVariables = OgnlHelper.extractVariables(collectionExpression);
            set.addAll(collectionVariables);
            Set<String> contentSet = new HashSet<>();
            {
                // extract contents' variables
                SqlNode contents = ReflectUtils.getDeclaredFieldValue(ForEachSqlNode.class, "contents", forEachSqlNode);
                String item = ReflectUtils.getDeclaredFieldValue(ForEachSqlNode.class, "item", forEachSqlNode);
                String index = ReflectUtils.getDeclaredFieldValue(ForEachSqlNode.class, "index", forEachSqlNode);
                extractVariables(contents, contentSet);
                contentSet.remove(item);
                contentSet.remove(index);
            }
            set.addAll(contentSet);
        } else if (sqlNode instanceof ChooseSqlNode) {
            ChooseSqlNode chooseSqlNode = (ChooseSqlNode) sqlNode;
            List<SqlNode> ifSqlNodes = ReflectUtils.getDeclaredFieldValue(ChooseSqlNode.class, "ifSqlNodes", chooseSqlNode);
            SqlNode defaultSqlNode = ReflectUtils.getDeclaredFieldValue(ChooseSqlNode.class, "defaultSqlNode", chooseSqlNode);
            if (CollectionUtils.isNotEmpty(ifSqlNodes)) {
                for (SqlNode ifSqlNode : ifSqlNodes) {
                    extractVariables(ifSqlNode, set);
                }
            }
            extractVariables(defaultSqlNode, set);
        } else if (sqlNode instanceof IfSqlNode) {
            IfSqlNode ifSqlNode = (IfSqlNode) sqlNode;
            String test = ReflectUtils.getDeclaredFieldValue(IfSqlNode.class, "test", ifSqlNode);
            Set<String> testVariables = OgnlHelper.extractVariables(test);
            set.addAll(testVariables);
            SqlNode contents = ReflectUtils.getDeclaredFieldValue(IfSqlNode.class, "contents", ifSqlNode);
            extractVariables(contents, set);
        } else if (sqlNode instanceof TrimSqlNode) {
            // including WhereSqlNode and SetSqlNode
            TrimSqlNode trimSqlNode = (TrimSqlNode) sqlNode;
            SqlNode contents = ReflectUtils.getDeclaredFieldValue(TrimSqlNode.class, "contents", trimSqlNode);
            extractVariables(contents, set);
        } else if (sqlNode instanceof StaticTextSqlNode) {
            StaticTextSqlNode staticTextSqlNode = (StaticTextSqlNode) sqlNode;
            String text = ReflectUtils.getDeclaredFieldValue(StaticTextSqlNode.class, "text", staticTextSqlNode);
            Set<String> variables = extractVariablesFromStaticText(text);
            set.addAll(variables);
        } else if (sqlNode instanceof TextSqlNode) {
            TextSqlNode textSqlNode = (TextSqlNode) sqlNode;
            String text = ReflectUtils.getDeclaredFieldValue(TextSqlNode.class, "text", textSqlNode);
            Set<String> variables = extractVariablesFromStaticText(text);
            set.addAll(variables);
        }
        // else { /* 不处理其他SqlNode */ }
        return set;
    }

    private static Set<String> extractVariablesFromStaticText(String text) throws OgnlException {
        if (StringUtils.isBlank(text)) {
            return Collections.emptySet();
        }
        Set<String> variables = new HashSet<>();

        Matcher matcher = MYBATIS_VARIABLE_PATTERN.matcher(text);
        while (matcher.find()) {
            // Extract the variable inside #{}
            String variable = null;
            if (matcher.group(1) != null && !matcher.group(1).isEmpty()) {
                variable = matcher.group(1);
            } else if (matcher.group(2) != null && !matcher.group(2).isEmpty()) {
                variable = matcher.group(2);
            }

            if (variable != null) {
                // Extract top-level variable for properties like map.propertyKey
//                String topLevelVariable = variable.split("\\.")[0];
                Set<String> topLevelVariable = OgnlHelper.extractVariables(variable);
                variables.addAll(topLevelVariable);
            }
        }

        return variables;
    }

    private static SqlSource parseSqlScript(String xmlString) {
        xmlString = "<script>" + xmlString + "</script>";
        XPathParser parser;
        try {
            parser = new XPathParser(xmlString, false, CONFIGURATION.getVariables(), new XMLMapperEntityResolver());
        } catch (Exception e) {
            throw new RuntimeException("SQL模板的XML格式错误", e);
        }
        XMLScriptBuilder builder = new XMLScriptBuilder(CONFIGURATION, parser.evalNode("/script"), Map.class);
        return builder.parseScriptNode();
    }

    private static Object[] parseArgs(BoundSql boundSql) {
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (CollectionUtils.isEmpty(parameterMappings)) {
            return ZERO_LENGTH_ARGS;
        }
        Object[] args = new Object[parameterMappings.size()];
        Object parameterObject = boundSql.getParameterObject();
        for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i);
            if (parameterMapping.getMode() != ParameterMode.OUT) {
                Object value;
                String propertyName = parameterMapping.getProperty();
                if (boundSql.hasAdditionalParameter(propertyName)) {
                    value = boundSql.getAdditionalParameter(propertyName);
                } else if (parameterObject == null) {
                    value = null;
                } else {
                    MetaObject metaObject = CONFIGURATION.newMetaObject(parameterObject);
                    value = metaObject.getValue(propertyName);
                }
                args[i] = value;
            }
        }
        return args;
    }

    private MyBatisUtils() {
    }

}
